using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using SavedVariablesParser.Line;

namespace SavedVariablesParser {
    /// <summary>
    /// An object that represents a World of Warcraft Saved Variables .lua file.
    /// </summary>
    public sealed class Document {
        private string path = string.Empty;
        private int lineCount = 0;
        private string addonName = string.Empty;
        ILineCollection lines = new ILineCollection();
        private string xml = string.Empty;
        private DateTime startTime = DateTime.MinValue;
        private DateTime endTime = DateTime.MinValue;

        /// <summary>
        /// Represents the method that will handle the <see cref="LineParsed"/> event. This class cannot be inherited. 
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="line">An <see cref="ILine"/> object that represents the data from the event.</param>
        public delegate void LineParsedEventHandler(Document sender, ILine line);
        /// <summary>
        /// The event that is fired whenever a line is parsed by the <see cref="Document"/>.
        /// </summary>
        public event LineParsedEventHandler LineParsed;

        /// <summary>
        /// 	<para>Initializes an instance of the <see cref="Document"/> class.</para>
        /// </summary>
        /// <param name="path">The path to the saved variables .lua file.</param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// 	<para>The argument <paramref name="path"/> is out of range.</para>
        /// </exception>
        /// <exception cref="FileNotFoundException"></exception>
        public Document(string path) {
            if (string.IsNullOrEmpty(path)) {
                throw new ArgumentOutOfRangeException("path", path, "Path cannot be null or a zero length string.");
            }
            if (!File.Exists(path)) {
                throw new FileNotFoundException("Saved Variables File Not Found.", path);
            }
            // make sure the file is a text file!
            // TODO:  code here for that check            
            
            this.path = path;
            // determine the addon name (the name of the file without its extension).
            this.addonName = System.IO.Path.GetFileNameWithoutExtension(this.path);
            // get the number of lines in the file
            this.lineCount = this.CountLines(this.path);
        }

        /// <summary>
        /// Returns the number of lines in the original .lua file.
        /// </summary>
        public int LineCount {
            get {
                return this.lineCount;
            }
        }

        /// <summary>
        /// Returns a collection of line objects that represent the lines of the .lua file.
        /// </summary>
        public ILineCollection Lines {
            get {
                return this.lines;
            }
        }

        /// <summary>
        /// Returns the name of the addon that the document represents.
        /// </summary>
        public string AddonName {
            get {
                return this.addonName;
            }
        }

        /// <summary>
        /// Returns the path to the original .lua document.
        /// </summary>
        public string Path {
            get {
                return this.path;
            }
        }

        /// <summary>
        /// Returns the amount of time that the conversion operation took to complete.
        /// </summary>
        public TimeSpan Duration {
            get {
                return this.endTime.Subtract(this.startTime);
            }
        }

        /// <summary>
        /// Converts the .lua document to an xml string.
        /// </summary>
        /// <returns>A string containing the xml that represents the structure of the .lua file.</returns>
        /// <example>This example shows how to execute the ConvertToXML method:
        /// <code>
        /// Document doc = new Document("C:\pathtofile\myluafile.lua");
        /// string xml = doc.ConvertToXML();
        /// Console.WriteLine(xml);
        /// </code>
        /// </example>
        public string ConvertToXML() {
            StringBuilder builder = new StringBuilder();
            string parsedLine = string.Empty;
            ILine line;
            int linePosition = 0;
            this.startTime = DateTime.Now;

            this.GenerateBeginningRootNode(this.addonName, builder); // generate the root node of the document

            using (StreamReader sr = new StreamReader(this.path)) {
                // loop over each line in the file.
                while ((parsedLine = sr.ReadLine()) != null) {
                    linePosition++; // increment the current line position.
                    if (parsedLine.Length > 0) { // only parse lines that contain data.
                        line = Line.LineFactory.CreateLine(parsedLine, linePosition); // use the factory to generate the line object.
                        line.Render(builder); // render the line using the supplied string builder.
                        this.lines.Add(line); // add the line to the internal lines collection.
                        OnParsed(line); // fire the event.
                    }
                }
            }

            this.GenerateEndRootEndNode(builder); // generate the end node of the document.

            this.xml = builder.ToString();

            this.endTime = DateTime.Now;

            return this.xml;
        }

        /// <summary>
        /// Converts the .lua document to an <see cref="XmlDocument"/>.
        /// </summary>
        /// <returns>The converted .lua file represented as an <see cref="XmlDocument"/></returns>
        public XmlDocument ConvertToXMLDocument() {
            if (xml.Length == 0) {
                this.ConvertToXML();
            }
            XmlDocument document = new XmlDocument();
            document.LoadXml(this.xml);
            return document;
        }

        /// <summary>
        /// Saves the converted file to the path specified.
        /// </summary>
        /// <param name="path">The path to save the document to.</param>
        public void SaveConvertedXML(string path) {
            XmlTextWriter writer = new XmlTextWriter(path, Encoding.UTF8);
            writer.Formatting = Formatting.Indented;
            this.ConvertToXMLDocument().WriteContentTo(writer);
            writer.Flush();
            writer.Close();
        }

        /// <summary>
        /// Saves the converted file to the same path as the original file with a .xml extension.
        /// </summary>
        /// <returns>The path where the file was saved.</returns>
        public string SaveConvertedXML() {
            string savedPath = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(this.path), System.IO.Path.GetFileNameWithoutExtension(this.path) + ".xml");
            this.SaveConvertedXML(savedPath);
            return savedPath;
        }

        /// <summary>
        /// Delegate method for the LineParsed event.
        /// </summary>
        /// <param name="line">The line that was parsed.</param>
        public void OnParsed(ILine line) {
            if (LineParsed != null) {
                LineParsed(this, line);
            }
        }

        /// <summary>
        /// Generates the beginning root node tag for the document.
        /// </summary>
        /// <param name="addonName">The name of the addon being converted.</param>
        /// <param name="builder">The string builder that the tag should be written to.</param>
        private void GenerateBeginningRootNode(string addonName, StringBuilder builder) {
            // <?xml version="1.0" encoding="utf-8" ?>
            // <addon name="auctioneer">
            builder.Append("<?xml version=\"1.0\" encoding=\"utf-8\" ?>");
            builder.Append(Environment.NewLine);
            builder.Append("<addon name=\"");
            builder.Append(addonName);
            builder.Append("\">");
            builder.Append(Environment.NewLine);
        }

        /// <summary>
        /// Generates the end root node tag for the document.
        /// </summary>
        /// <param name="builder">The string builder that the tag should be written to.</param>
        private void GenerateEndRootEndNode(StringBuilder builder) {
            builder.Append("</addon>");
            builder.Append(Environment.NewLine);
        }

        /// <summary>
        /// Counts the number of lines in a text file.
        /// </summary>
        /// <param name="path">The path the file.</param>
        /// <returns>The number of lines within the file.</returns>
        private int CountLines(string path) {
            int lines = 0;
            using (StreamReader sr = new StreamReader(this.path)) {
                while ((sr.ReadLine()) != null) {
                    lines++;// increment the number of lines found.
                }
            }
            return lines;
        }

        #region Equality Methods

        /// <summary>
        /// Determines if two <see cref="Document"/> objects have the same value.
        /// </summary>
        /// <param name="x">The first <see cref="Document"/> object to compare.</param>
        /// <param name="y">The second <see cref="Document"/> object to compare.</param>
        /// <returns>true if the first <see cref="Document"/> is equal to the second <see cref="Document"/>; otherwise, false.</returns>
        /// <filterpriority>2</filterpriority>
        public static bool Equals(Document x, Document y) {
            if ((object)x == (object)y) {
                return true;
            }

            if ((object)x == null || (object)y == null) {
                return false;
            }

            if (x.Path.Equals(y.Path)) {
                return true;
            }

            return false;
        }

        /// <summary>Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.</summary>
        /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
        /// <returns>true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.</returns>
        /// <filterpriority>2</filterpriority>
        public override bool Equals(object obj) {
            Document equals = obj as Document;
            if (equals != null) {
                return Document.Equals(this, equals);
            }
            return false;
        }

        /// <summary>
        /// Determines whether the specified <see cref="Document"/> is equal to the current <see cref="Document"/>.
        /// </summary>
        /// <param name="document">The <see cref="Document"/> to compare with the current <see cref="Document"/>.</param>
        /// <returns>true if the specified <see cref="Document"/> is equal to the current <see cref="Document"/>; otherwise, false.</returns>
        public bool Equals(Document document) {
            return Document.Equals(this, document);
        }

        /// <summary>
        /// Determines if two <see cref="Document"/> objects have the same value.
        /// </summary>
        /// <param name="x">The first <see cref="Document"/> object to compare.</param>
        /// <param name="y">The second <see cref="Document"/> object to compare.</param>
        /// <returns>true if the first <see cref="Document"/> is equal to the second <see cref="Document"/>; otherwise, false.</returns>
        public static bool operator ==(Document x, Document y) {
            return Document.Equals(x, y);
        }

        /// <summary>
        /// Determines if two <see cref="Document"/> objects have the same value.
        /// </summary>
        /// <param name="x">The first <see cref="Document"/> object to compare.</param>
        /// <param name="y">The second <see cref="Document"/> object to compare.</param>
        /// <returns>false if the first <see cref="Document"/> is equal to the second <see cref="Document"/>; otherwise, true.</returns>
        public static bool operator !=(Document x, Document y) {
            return !Document.Equals(x, y);
        }

        #endregion

        /// <summary>Serves as a hash function for a particular type. <see cref="M:System.Object.GetHashCode"/> is suitable for use in hashing algorithms and data structures like a hash table.</summary>
        /// <returns>A hash code for the current <see cref="Document"/>.</returns>
        /// <filterpriority>2</filterpriority>
        public override int GetHashCode() {
            return this.path.GetHashCode() ^ this.addonName.GetHashCode();
        }
    }
}